Skip to content

Viewer: address pages use Yaci Store's Blockfrost-compatible API when it is there - #219

Merged
satran004 merged 25 commits into
mainfrom
viewer/bf-address
Nov 9, 2026
Merged

satran004 merged 25 commits into
mainfrom
viewer/bf-address

Conversation

@satran004

@satran004 satran004 commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Stacked on #218. Part of the Yaci Viewer work in #207.

Where Yaci Store has its Blockfrost-compatible API (the playground does; the devkit's own store doesn't), address pages read from it. Elsewhere nothing changes.

Why

Yaci Store runs every Blockfrost-compatible address query under a 15 s statement timeout (QUERY_TIMEOUT_SECONDS). On a busy address they fail fast with a 500 and leave nothing running. The native /addresses/{a}/utxos (deep pages) and /addresses/{a}/transactions keep running after the viewer gives up (bloxbean/yaci-store#1269). In testing, native /transactions on the busy address was still running 15 minutes later, and deep native UTxO pages were still running 12 minutes later.

/blockfrost/addresses/{a} also gives an exact balance, where summing 5 UTxO pages could only give "≥" for addresses with more than 500 UTxOs.

What changes on the address page

Store with the Blockfrost-compatible API Store without (devkit)
Balance, when /balance isn't there One UTxO page holds them all: their sum (exact). Otherwise /blockfrost/addresses/{a} (exact); only if that fails, the sum of up to 5 pages (lower bound) As before
UTxOs tab and UTxO pages for the balance /blockfrost/addresses/{a}/utxos /addresses/{a}/utxos
Transactions tab /blockfrost/addresses/{a}/transactions /addresses/{a}/transactions
  • A 500 from the Blockfrost-compatible API (the store's timeout) is treated like a timeout: the key is skipped for a while, as the guard already does.
  • The store's 404 "No endpoint" means it has no such API: the native endpoints are used, and the store is checked again after 10 minutes.
  • The fields the page uses (tx_hash, output_index, amount, datums, block_height, block_time) are the same in both APIs.
  • The balance steps run one after the other, not in parallel, so a busy address gets one slow query at a time.
  • After the store's 15 s timeout, the call is skipped for at most a minute (the native endpoints keep their 2 or 15 minutes), and the "too slow" notice offers Try again, which clears the skip for that address. Its query has already stopped on the store, and a second try is often quick once the data is cached (one address went from 10 s cold to 0.9 s). The button is shown only where the calls go to the Blockfrost-compatible API.

Measured on Mainnet (yaci-playground, via the VM)

Address Before After
10 UTxOs exact (one page) exact (one page), unchanged
More than 500 UTxOs (5 addresses from recent blocks) "≥", sum of the newest 500 exact, /blockfrost/addresses/{a} in 0.07–0.2 s
Large script address "≥" exact (Blockfrost balance in 10.9 s); transactions: "too slow" after 15 s
The busy address (1.9M outputs) lower bound; transactions left a query running lower bound after ~40 s on the first visit, instant after; transactions "too slow" after 15 s, nothing left running

For comparison, native /amounts took 8 s for the 10-UTxO address, and /blockfrost/addresses/{a} 0.36 s.

/blockfrost/addresses/{a}/total isn't used: when it hits the store's timeout it answers 200 with empty sums instead of an error.

Checks

  • svelte-check: 0 errors. Unit tests: 170 pass (new: answers, "No endpoint" detection and back-off, a 500 marking the key heavy, presence tracking, the guard forgetting a key, other errors passed on).
  • Production build against the Mainnet store: the addresses above; after the run, no query from it still active in Postgres.
  • Against the devkit's devnet store (no Blockfrost API): balance and transactions as before, ~0.15 s.

🤖 Generated with Claude Code

satran004 and others added 23 commits October 17, 2026 00:58
- Design tokens (light and dark) on Tailwind, matching the DevKit brand;
  self-hosted Inter, Space Grotesk and JetBrains Mono for offline devnets
- App shell: collapsible sidebar grouped by task, top bar with search,
  network and live status, theme toggle, page progress bar, view
  transitions; replaces the full-screen spinner on every navigation
- Search everywhere (Cmd/Ctrl+K, /, or paste): classifies the input and
  checks ambiguous hashes against the store
- API client for server loads and the browser (honours
  PUBLIC_INDEXER_CLIENT_BASE_URL), BigInt-safe formatting
- Live layer: WebSocket with reconnect and backoff, polling fallback
  for stores without the live module; reads aggregate_data correctly
- Dashboard: live block conveyor with a forging countdown, block time,
  tx and block-load trends, latest transactions, epoch ring, supply
- Upgrade Svelte 5.16 -> 5.57 and Kit 2.15 -> 2.70 (hydration fixes)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Transaction page: visual UTxO flow (inputs -> tx -> outputs) with
  change detection and address highlighting, where-the-ADA-went
  breakdown, scripts with execution budget bars, decoded redeemers and
  datums (Plutus data CBOR decoder), mint/burn, metadata with CIP
  labels, collateral, reference inputs, required signers, raw JSON
- Block page: prev/next with arrow keys, live confirmations, size gauge,
  consensus fields on demand; blocks list with size bars and live rows
- Transactions list with live rows on page 1
- Address page: type from the header, balance (with /amounts fallback),
  stake account and pool derived via bech32, txs/UTxOs/tokens tabs
- Stake account page: controlled stake, pool and DRep delegation,
  rewards, withdrawals, other rewards
- Fix paging: these Yaci Store endpoints are 1-based, so page 2 of an
  address or stake account used to show page 1 again
- CIP-68 asset names, signed mint quantities, address lookup page,
  friendly 404/503 page
- Theme follows the OS until the user picks one
- Remove the old home-page widgets and WebSocket store

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New and redesigned pages
- Epochs (new): list with live progress for the current epoch, and an
  epoch page with blocks and grouped, filterable protocol parameters
- Assets: unit/policy/fingerprint pages with mint and burn history,
  CIP-68 names, supply; scripts (native script tree, CBOR) and datums
- Ada pots with trend charts; withdrawals
- Pools: lookup plus recent registrations and retirements; pool page
  with metadata, stake trend, delegators, blocks and rewards
- Rewards, epoch stakes (fix the doubled /api/v1 in its loader and the
  browser fetches that ignored the client base URL), and one design for
  the five certificate lists
- Governance: overview, proposals (govactions redirects there), action
  page with a vote tally, DReps, votes, committee, constitution and DRep
  activity

Security
- The governance anchor page fetched any URL on the server. Anchors and
  pool metadata now go through one safe fetcher: http(s) on 80/443 and
  ipfs only, no private/loopback/link-local/reserved addresses (also
  checked after DNS, at connect time, and on every redirect), size and
  time limits, JSON only. Anchors are checked against their on-chain
  BLAKE2b-256 hash

Fixes and cleanup
- API timeouts now also race the request (SvelteKit's server fetch can
  ignore the abort signal); the live clock reads real time on the server
- Remove DaisyUI, the old components, utilities, stores and dead routes;
  drop moment, svelte-feather-icons, classnames and @popperjs/core
- Docker image runs the production build (node build) on 5173 instead of
  the Vite dev server; the browser store URL follows HOST_STORE_API_PORT
- Playwright smoke tests that actually run (19 pages and flows), unit
  tests for formatting, search, CBOR, bech32, addresses and URL safety
- README rewritten

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Script pages can now decode Plutus scripts in the browser: searchable, highlighted UPLC, the builtins
used (click to find them), size, term count, parameters applied and a hash check.

- julc is not built here: scripts/fetch-julc.mjs downloads the release pinned in julc.lock.json,
  checks its SHA-256 and unpacks plain files into static/julc/ (git-ignored). It runs before dev
  (optional) and build (required unless JULC_SKIP=1; JULC_ARCHIVE installs from a local copy), so
  Docker, Earthly and npm builds all include it.
- $lib/julc/engine: lazy, shared worker client. Nothing loads until the user decodes (or opts in to
  always decoding); WasmGC and exnref are checked first, so older browsers get a message instead of
  a 5.5 MB download that can't run.
- CodeView: virtualised, so 20k-line scripts stay fast; search with match navigation, wrap, copy,
  download.
- Tests: untar safety and highlighting (vitest); tests/script-lab.spec.ts (needs SCRIPT_LAB_HASH and
  PW_CHANNEL=chrome, as Playwright's bundled Chromium lacks exnref).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Scripts tab of a transaction can now re-run every script with julc: the transaction is rebuilt
from Yaci Store data and each redeemer runs with the cost model and protocol version of its epoch.
Results show julc's budget next to the declared one ("Reproduced exactly", or reproduced with the
builder's margin), traces, the failure and last builtins, and the exact ScriptContext.

- replay/build.ts: MockTransaction per redeemer in ledger order (inputs, values, mint, withdrawals,
  votes), slot -> POSIX time (Shelley constants for public networks, /genesis for devnets),
  epoch cost models and tx limits; explains runs it can't rebuild (Byron addresses, proposals,
  unsupported certificates) and adds notes on known gaps.
- replay/fix-context.ts: workarounds until julc builds ledger-exact contexts (julc#244): asset
  values in ledger order, the complete redeemer map (withdraw-zero validators), datums given by
  hash, and V1 list shapes. Corrected contexts are run again with the VM; each result says what
  changed.
- cbor.ts: Plutus data encoder (Plutus-library style), round-trip tested.
- load.ts: fetches the rest only on click; certificates and votes only when a script needs them;
  falls back to the latest parameters when an epoch's are missing.

Mainnet checks: 40 recent transactions, 108 runs, all passed (80 exact, 5 with builder margin).
2023 Plutus V1 history: 8 of 17 runs pass; julc's V1 fixes after pre18 should improve this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/transactions/{hash}/debug/{purpose}/{index} replays one script run in julc's VM with exactly the
context the Scripts tab used, and lets you move through it:

- step, step over, step out, continue, step back, start/end; a timeline to click or drag, with
  trace and failure markers; keyboard shortcuts
- breakpoints on UPLC lines (click a line number), builtins, traces and failures
- the current term highlighted in the UPLC and followed vertically and horizontally
- now (phase, term, value, why it stopped), budget at the step against the run and the declared
  ex-units, environment, frames and traces so far
- ?step=N links to an exact machine state; ?at=failure opens at the failing step
- "Debug" / "Debug the failure" links on every re-run in the Scripts tab

The re-run pipeline moved to replay/run.ts so the tab and the debugger share it, and every result
keeps the VM arguments it was run with. The transaction loader moved to $lib/tx/load.server.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ipt Lab)

/transactions/{hash}/fork rebuilds a transaction and lets you change it: redeemers and spent
datums (JSON in the detailed schema, or CBOR), required signers, the validity range, outputs
(address, ADA, tokens, removal), mint, withdrawals, fee, and the rules (another epoch's cost
model, protocol 10 or 11). Every script runs again shortly after each edit, next to the on-chain
result, with CPU/memory changes and "now passes" / "now fails" when an outcome flips.

- replay/fork.ts applies edits to the store-shaped transaction before it is rebuilt, so ledger
  order, purposes, the julc#244 context fixes and the redeemer map stay consistent.
- Forked runs open in the debugger inline (it now accepts a prepared run).
- Any run, on chain or forked, exports as a JSON fixture: script, target, cost model, limits,
  VM arguments, ScriptContext CBOR, expected result and the fork's edits.
- Older epochs' cost models are shorter; the lab says when julc fills the missing builtin costs.
- "Fork this transaction" link on the Scripts tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ipt Lab)

- Script monitor (/scripts/monitor, in the nav): every new block's script transactions are re-run
  as they arrive, one at a time, with pass/fail, reproduced budgets, CPU/memory against the
  per-transaction limits and a flag near them; each run links to the debugger and the fork lab.
  All script transactions on devnets; a few per block on public networks (configurable).
- Script activity on the script page (streamed from the server): recent executions, median and
  peak budgets, a chart of declared CPU, redeemer variants with examples, and Debug links.
- Blueprints: attach a CIP-57 plutus.json on a script page (kept in this browser). Matching
  datums and redeemers get a Typed view (type, variant and field names, with mismatches marked),
  and activity names redeemer variants. Validators are matched by hash and handler (Aiken lists
  one entry per handler); a missing hash is computed from the compiled code.
- Shared verdict helpers (passed / reproduced exactly / with margin) and a client-side loader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Guardrail check (/governance/guardrail): draft a parameter change or treasury
  withdrawal and run the constitution's guardrail script on it in the browser.
  The verdict updates as you type, each changed parameter is also checked on its
  own so a rejection names the value the guardrail doesn't allow, the run opens
  in the debugger, and the draft is kept in a shareable URL. Start from a recent
  proposal, or check against another script (a hash on the chain or pasted CBOR).
- Proposal pages: a "Proposed changes" panel (current value against proposed)
  that runs the proposal's guardrail, with per-parameter verdicts and a link to
  try changes.
- Proposing scripts are now re-run on the Scripts tab, the debugger and the fork
  lab. julc 0.1.0-pre18 builds parameter changes without their changes, so the
  viewer encodes the governance action from Yaci Store data (ledger key order,
  rationals, ex-units, prices, thresholds, cost models; treasury withdrawals in
  credential order) and puts it into the proposal list, the redeemer purpose and
  the ScriptInfo.
- Mainnet: 49 of 49 guardrail runs pass; 25 match the on-chain budget exactly,
  1 within the builder's margin. Runs from protocol 10 epochs use about 31k more
  CPU than declared (memory exact); julc costs protocol 10 like 11, so those
  runs carry a note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Validator lab (/scripts/lab, "Validator lab" in the sidebar, and a link on
  Plutus script pages): bring a validator as julc Java (compiled in the browser,
  with diagnostics, parameters, compiled UPLC and plutus.json), as a pasted
  compiled script (V1-V3) or from this chain by hash.
- Point it at chain state: spend a real UTxO (by tx#index, or picked from an
  address) as if it were locked at the validator, mint under its policy, or
  withdraw from its stake credential, with the redeemer, datum, signers,
  validity range, other withdrawals and outputs you set. Starting points come
  from julc: datum and redeemer templates per variant, typed views from the
  compiled blueprint, the UTxO's own datum and owner key.
- The verdict, budget against the tx limits and traces update as you type. A
  rejected Java validator shows the line it failed on (from julc's source-debug
  build), marked in the editor. Runs open in the UPLC debugger or an
  experimental Java source debugger (Java lines, line breakpoints, locals where
  julc 0.1.0-pre18 provides them), and export as test fixtures.
- Runs go through the re-run pipeline (context fixes, verdicts, fixtures), with
  the chain's cost model and limits. A Mainnet batcher (Plutus V2) run on the
  UTxO it really spent, with its withdraw-zero stake script, reproduces its
  on-chain budget exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SvelteKit sends a page's CSS and JS preloads as one Link response header. On
larger pages it passes 4 KB, the default nginx proxy_buffer_size, so the viewer
behind a default reverse proxy failed with 502 "upstream sent too big header"
(seen on yaci-viewer.cf-app.org). A server hook now writes the same preloads
into <head> as <link> tags while the HTML streams through, and drops the header
(response headers go from ~5 KB to ~200 bytes; streamed pages still stream).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Transaction decoder (src/lib/tx): a general CBOR reader that keeps byte
  ranges, and a Cardano transaction decoder (Alonzo to Conway encodings): body,
  witnesses, outputs (legacy and map), certificates, withdrawals, votes,
  proposals, redeemers (list and map forms), metadata. Transaction ids, datum
  and script hashes are computed from the exact bytes.
- Replays use the transaction's own CBOR when Yaci Store saved it
  (/txs/{hash}/cbor/hex, store.transaction.save-cbor): every redeemer and its
  script, witnessed datums, outputs as encoded, certificates, votes, and
  proposals encoded straight from their CBOR. Otherwise they are rebuilt from the
  store's JSON as before. This covers the Scripts tab, debugger, fork lab and
  script monitor (all go through loadReplayInput).
- Every replay says where it came from: "From the transaction's CBOR", or
  "Rebuilt from Yaci Store data" with the reason (the store doesn't keep
  transaction CBOR). Fixtures record it too.
- Transaction pages get a CBOR tab when the store returns the bytes (streamed,
  the page doesn't wait): checked against the transaction id, shown as a tree
  with the ledger's field names and byte ranges, copy or download as .cbor.
- On 14 Mainnet transactions (V1-V3; spend, mint, withdraw, vote, proposals;
  Alonzo and Conway encodings) replays from CBOR describe the same runs as the
  store path and pass; a V2 mint whose quantity the store's JSON rounds
  (2^63 - 1) now reproduces exactly, where the rebuilt one failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hidden tab doesn't poll, and a sleeping computer drops the socket. On
return, the viewer put the new tip in front of blocks from hours before,
so the dashboard's block time averaged over the gap (thousands of seconds)
until a page reload.

Now a gap of up to 16 blocks is filled in, a longer one reloads the recent
blocks and transactions, and the viewer catches up as soon as the tab is
visible again, with or without the socket. The block list only ever holds
consecutive blocks, so no average spans a gap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a Mainnet-sized Yaci Store, /addresses/{a}/amounts, /addresses/{a}/transactions
and /accounts/{stake} can take hours for busy addresses, and the store keeps running
the query after the viewer gives up (bloxbean/yaci-store#1269). Every page view
started another one, and the page waited for the slowest call.

- Address and stake pages wait only for /balance; the balance, lists and stake
  account stream in, each with its own loading, timeout and error state.
  X-Accel-Buffering: no lets the stream through nginx.
- No /amounts: without /balance, the balance is summed from up to 5 UTxO pages,
  exact when a page comes back short, otherwise shown as a lower bound (≥).
- A guard on the server: an endpoint that timed out for an address is skipped for
  15 minutes (a UTxO page for 2), identical requests in flight share one call,
  stake accounts and balance pages are kept for a minute, and at most 4 slow calls
  run at once. A busy address opens on its UTxOs tab.
- The balance shows ADA to 2 decimals, with the full lovelace amount below.
- robots.txt keeps crawlers off per-address pages.
- Limits in src/lib/server/heavy-guard.ts; HEAVY_ADDRESS_GUARD=false restores the
  plain behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where the store has its analytics query API (POST /analytics/query/sql, DuckDB over
its daily exports, joined with live PostgreSQL), the sections that are too slow for
busy addresses come from there instead of a notice:

- an address's transactions: those that paid it (live) or spent from it (up to the
  last export), newest first, searched over 7, 30 then 90 days, with Older/Newest
  paging by a day and slot cursor;
- a stake account's controlled stake and pool, as of the last export, and its
  rewards and other rewards, 20 epochs a page.

The regular endpoint is still asked first; analytics answers when the guard finds it
too slow. Analytics queries are bounded by the store's 30 s timeout, go through
their own guard (two at a time, answers kept 10 minutes, a timed-out query skipped
for 15), and read the exports only, except address_utxo and reward, until
bloxbean/yaci-store#1270 is resolved. Every value written into SQL is checked
against a strict pattern. The store is probed every 10 minutes; the devkit's store
answers 404, and nothing changes there. YACI_ANALYTICS=off turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reverse proxy can keep the analytics API internal (403 to outside callers). Treat
that like 404: nothing to use until the next probe in 10 minutes, instead of
probing again every minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DuckDB over Parquet isn't meant to serve page views: each query takes seconds of
CPU across the store's cores, and an address's transactions from analytics are
partial (spends only up to the last export, of outputs from the last 90 days).
Address pages go back to #215's behaviour, notices included. Analytics stays for
busy stake accounts (controlled stake, pool, rewards by epoch), which are cheap,
exact per epoch and cache well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd stake pages

Where the store has Yaci Store analytics, pages get charts and numbers from its
daily exports, which DuckDB aggregates in well under a second:

- Dashboard: transactions, script runs, script CPU and fees per day (90 days);
  treasury and reserves; the Nakamoto coefficient; pools making blocks; the
  most-run scripts (7 days).
- Governance: voting power, the 10 largest DReps and options.
- Script: runs, CPU steps and the costliest run per day (90 days).
- Pool: blocks, active stake and delegators per finished epoch (50 epochs).
- Stake account: rewards and active stake per epoch (50 epochs).

Answers are kept in memory and served while they refresh in the background
(network-wide for an hour, per script, pool or account for 30 minutes), in a small
stale-while-revalidate cache with an entry limit. Sections stream in and are left
out where the store has no analytics (the devkit's). Values written into SQL are
checked against strict patterns. Charts are plain SVG (Chart.svelte), no new
dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removes the card and the stake concentration query behind it, so the dashboard no
longer runs a query it doesn't show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Mainnet, ids get a name next to them:
- script hashes and script addresses: dApp labels from Strica's Cardano
  Contracts Registry and Cardano Fans' registry (Strica's wins on overlap);
- DReps: givenName from their CIP-119 metadata;
- pools: ticker from their CIP-6 metadata;
- governance actions: title from their CIP-108 metadata;
- tokens: ticker and decimals from the Cardano token registry, applied to
  every token chip and the asset page.

The server checks the store's network_magic first; on any other network
nothing is fetched and the browser stops asking. Names load in the
background (registries daily, DReps and actions every 6 h, pools every 12 h
with an incremental registrations crawl; tokens on demand, batched) and are
saved as JSON in NAMES_DIR, so a restart has them at once. Owner-written
names are kept only when the document matches the hash on chain, are
fetched through safeGet, and are labelled as self-declared. NAMES=off
turns it all off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The transaction list can take minutes for a busy address on a large store,
while the UTxOs come back quickly, so UTxOs is the first tab and the default,
and Transactions is the last one, loaded only when it is opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… it is there

Yaci Store runs its Blockfrost-compatible address queries under a 15 s
statement timeout, so on a busy address they fail fast instead of running on
after the viewer gives up, as the native /utxos and /transactions do
(yaci-store#1269). Where the store has that API:
- the balance past one page of UTxOs is the exact /blockfrost/addresses/{a}
  (0.07-0.2 s for addresses with over 500 UTxOs, which showed "≥" before);
  summing up to 5 pages is only the fallback when it fails;
- the UTxO pages and the transaction list come from it too.
A 500 from it (the store's timeout) is treated like a timeout: the key is
skipped for a while. Stores without it (the devkit's, which answers 404 "No
endpoint") get the native endpoints, checked again every 10 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copy link
Copy Markdown

Initial automated review · Aster

Hi @satran004, thanks for the PR! 🙏 This is an automatic first check by an AI reviewer. It does not replace maintainer review and may be wrong.

Reviewed commit: f30e51cc429bef4daf43e0dbf4060c462b44570f

Issues found:

  • applications/viewer/src/lib/server/blockfrost.ts:35 — An encoded slash in an address can trigger a route-miss 404 and disable Blockfrost for all visitors for ten minutes, sending them to the native endpoints. Could we validate the address before letting its 404 set this global flag?

Not tested: Unit tests, builds, or live-store behavior; this was a source-only review.

satran004 and others added 2 commits October 19, 2026 19:42
… try again

A Blockfrost-compatible call that hits Yaci Store's own 15 s timeout has
already stopped on the store, and a second try is often quick once the data is
cached. So such a call is skipped for at most a minute (was 2 or 15), and the
"too slow" notice offers "Try again", which clears the skip for that address.
The button is shown only where the calls go to the Blockfrost-compatible API:
the native endpoints keep running on the store, so they keep the long skip.
The retry parameter is removed from the address bar after the page loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security
- Store paths built from route params can no longer leave the API: SvelteKit
  decodes %2F, %3F and %23, so /blocks/..%2F..%2Factuator%2Fenv fetched the
  store host's /actuator/env and put it in the page. The API client refuses
  `..` (also %2e and with tabs), `?`, `#` and backslashes, and checks the URL
  stays under the API base. Block, transaction, address and stake routes
  check their id's shape and answer 404 otherwise.
- The Blockfrost "absent" flag can no longer be set by a crafted address (the
  address is validated first).
- Governance anchors: at most 6 lookups at once, results kept 10 minutes.
- Token registry: at most 2 requests in flight; cache eviction drops
  "not registered" entries before real names.
- Deep pages of an address's transactions or a stake account's lists have their
  own guard key, so one slow ?page=N can't mark the list for every visitor.
- Pool metadata link only for http(s); stake ?tab= and proposal ?type= checked
  with Object.hasOwn; a stray % in a governance action id is a 404, not a 500.

Correctness
- Failed transactions: Yaci Store's /txs/{hash} always says invalid=false. A
  failed transaction's collateral return is kept at index outputs.length with
  is_collateral_return, so the page now shows it as failed.
- The collateral return comes in the store's own shape (amounts, camelCase): it
  is mapped to a UTxO, so it shows its ADA and assets.
- Asset holders: /assets/{unit}/addresses is zero-based in Yaci Store 2.0.x;
  page 1 skipped the first holders.
- Live chain: a devnet reset or a rollback below the tip reloads the recent
  blocks (also when the socket reconnects), instead of staying on the old tip;
  after a catch-up over several blocks, the missed transactions are loaded.
- Stored CBOR counts only when it is a whole transaction: Yaci Store 2.0.x saves
  the body only, which showed "Couldn't decode" and can't be replayed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@satran004

Copy link
Copy Markdown
Member Author

Added 7e6605c, Viewer: fixes from the stack review. It fixes issues found while reviewing the whole stack (#204 → #219), including the Aster findings on #204 and #219.

Security

  • Path traversal: route params went into store URLs unencoded. /blocks/..%2F..%2F..%2Factuator%2Fenv made the server fetch the store host's /actuator/env and put the response in the page.
    • The API client now refuses .. (also %2e, or with tabs), ?, # and \, and checks the URL stays under the API base.
    • The block, transaction, address and stake routes check their id's shape (404 otherwise).
    • This also stops a crafted address from setting the Blockfrost "absent" flag for everyone.
  • Governance anchors: at most 6 lookups at once; results are cached for 10 minutes.
  • Token registry: at most 2 requests in flight; cache eviction drops "not registered" entries first.
  • Deep pages: address transaction lists and stake lists get their own guard key per deep page, so one slow ?page=N can't mark the whole list as heavy.
  • Smaller fixes:
    • the pool metadata_url link only renders for http(s);
    • the stake ?tab= and proposal ?type= checks use Object.hasOwn;
    • a stray % in a gov action id now gives a 404, not a 500.

Correctness

  • Failed transactions showed "Success": Yaci Store 2.0.x never sets invalid on /txs/{hash}. The viewer now detects a failed transaction from its collateral-return UTxO, which the store keeps at index outputs.length with is_collateral_return. Checked on Mainnet: failed txs 434167d3… and d7ad7bbe… now show as failed, and successful script txs with a collateral return still show "Success".
  • Collateral return: it arrives as camelCase amounts and is now mapped to a UTxO, so its ADA and assets show.
  • Asset holders: /assets/{unit}/addresses is zero-based in the store, so page 1 skipped the first holders. The viewer now sends page - 1. Revert this when the store is fixed.
  • Live chain: a devnet reset, or a rollback below the tip, now reloads the recent blocks (also on WebSocket reconnect). After a multi-block catch-up, the missed transactions are fetched.
  • Stored CBOR: it only counts when it is a whole transaction. The store saves only the body, which showed "Couldn't decode".

Testing

  • npm run check: 0 errors.
  • Unit tests: 184 passing (14 new).
  • npm run build passes.
  • Ran the build against the Mainnet playground store; the main routes return 200.

🤖 Generated with Claude Code

@satran004
satran004 changed the base branch from viewer/names to main November 9, 2026 05:25
@satran004 satran004 closed this Nov 9, 2026
@satran004 satran004 reopened this Nov 9, 2026
@satran004
satran004 merged commit d93538a into main Nov 9, 2026
1 of 2 checks passed
@satran004
satran004 deleted the viewer/bf-address branch November 9, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants